//! Helpers for code generation that need struct layout

use super::helpers;

use crate::ir::comp::CompInfo;
use crate::ir::context::BindgenContext;
use crate::ir::layout::Layout;
use crate::ir::ty::{Type, TypeKind};
use crate::FieldVisibilityKind;
use proc_macro2::{Ident, Span};
use std::cmp;

const MAX_GUARANTEED_ALIGN: usize = 8;

/// Trace the layout of struct.
#[derive(Debug)]
pub(crate) struct StructLayoutTracker<'a> {
    name: &'a str,
    ctx: &'a BindgenContext,
    comp: &'a CompInfo,
    is_packed: bool,
    known_type_layout: Option<Layout>,
    is_rust_union: bool,
    can_copy_union_fields: bool,
    latest_offset: usize,
    padding_count: usize,
    latest_field_layout: Option<Layout>,
    max_field_align: usize,
    last_field_was_bitfield: bool,
    visibility: FieldVisibilityKind,
    last_field_was_flexible_array: bool,
}

/// Returns a size aligned to a given value.
pub(crate) fn align_to(size: usize, align: usize) -> usize {
    if align == 0 {
        return size;
    }

    let rem = size % align;
    if rem == 0 {
        return size;
    }

    size + align - rem
}

#[test]
fn test_align_to() {
    assert_eq!(align_to(1, 1), 1);
    assert_eq!(align_to(1, 2), 2);
    assert_eq!(align_to(1, 4), 4);
    assert_eq!(align_to(5, 1), 5);
    assert_eq!(align_to(17, 4), 20);
}

impl<'a> StructLayoutTracker<'a> {
    pub(crate) fn new(
        ctx: &'a BindgenContext,
        comp: &'a CompInfo,
        ty: &'a Type,
        name: &'a str,
        visibility: FieldVisibilityKind,
        is_packed: bool,
    ) -> Self {
        let known_type_layout = ty.layout(ctx);
        let (is_rust_union, can_copy_union_fields) =
            comp.is_rust_union(ctx, known_type_layout.as_ref(), name);
        StructLayoutTracker {
            name,
            ctx,
            comp,
            visibility,
            is_packed,
            known_type_layout,
            is_rust_union,
            can_copy_union_fields,
            latest_offset: 0,
            padding_count: 0,
            latest_field_layout: None,
            max_field_align: 0,
            last_field_was_bitfield: false,
            last_field_was_flexible_array: false,
        }
    }

    pub(crate) fn can_copy_union_fields(&self) -> bool {
        self.can_copy_union_fields
    }

    pub(crate) fn is_rust_union(&self) -> bool {
        self.is_rust_union
    }

    pub(crate) fn saw_flexible_array(&mut self) {
        self.last_field_was_flexible_array = true;
    }

    pub(crate) fn saw_vtable(&mut self) {
        debug!("saw vtable for {}", self.name);

        let ptr_size = self.ctx.target_pointer_size();
        self.latest_offset += ptr_size;
        self.latest_field_layout = Some(Layout::new(ptr_size, ptr_size));
        self.max_field_align = ptr_size;
    }

    pub(crate) fn saw_base(&mut self, base_ty: &Type) {
        debug!("saw base for {}", self.name);
        if let Some(layout) = base_ty.layout(self.ctx) {
            self.align_to_latest_field(layout);

            self.latest_offset += self.padding_bytes(layout) + layout.size;
            self.latest_field_layout = Some(layout);
            self.max_field_align = cmp::max(self.max_field_align, layout.align);
        }
    }

    pub(crate) fn saw_bitfield_unit(&mut self, layout: Layout) {
        debug!("saw bitfield unit for {}: {layout:?}", self.name);

        self.align_to_latest_field(layout);

        self.latest_offset += layout.size;

        debug!(
            "Offset: <bitfield>: {} -> {}",
            self.latest_offset - layout.size,
            self.latest_offset
        );

        self.latest_field_layout = Some(layout);
        self.last_field_was_bitfield = true;
        self.max_field_align = cmp::max(self.max_field_align, layout.align);
    }

    /// Returns a padding field if necessary for a given new field _before_
    /// adding that field.
    pub(crate) fn saw_field(
        &mut self,
        field_name: &str,
        field_ty: &Type,
        field_offset: Option<usize>,
    ) -> Option<proc_macro2::TokenStream> {
        let mut field_layout = field_ty.layout(self.ctx)?;

        if let TypeKind::Array(inner, len) =
            *field_ty.canonical_type(self.ctx).kind()
        {
            // FIXME(emilio): As an _ultra_ hack, we correct the layout returned
            // by arrays of structs that have a bigger alignment than what we
            // can support.
            //
            // This means that the structs in the array are super-unsafe to
            // access, since they won't be properly aligned, but there's not too
            // much we can do about it.
            if let Some(layout) = self.ctx.resolve_type(inner).layout(self.ctx)
            {
                if layout.align > MAX_GUARANTEED_ALIGN {
                    field_layout.size =
                        align_to(layout.size, layout.align) * len;
                    field_layout.align = MAX_GUARANTEED_ALIGN;
                }
            }
        }
        self.saw_field_with_layout(field_name, field_layout, field_offset)
    }

    pub(crate) fn saw_field_with_layout(
        &mut self,
        field_name: &str,
        field_layout: Layout,
        field_offset: Option<usize>,
    ) -> Option<proc_macro2::TokenStream> {
        let will_merge_with_bitfield = self.align_to_latest_field(field_layout);

        let is_union = self.comp.is_union();
        let padding_bytes = match field_offset {
            Some(offset) if offset / 8 > self.latest_offset => {
                offset / 8 - self.latest_offset
            }
            _ => {
                if will_merge_with_bitfield ||
                    field_layout.align == 0 ||
                    is_union
                {
                    0
                } else if !self.is_packed {
                    self.padding_bytes(field_layout)
                } else if let Some(mut l) = self.known_type_layout {
                    if field_layout.align < l.align {
                        l.align = field_layout.align;
                    }
                    self.padding_bytes(l)
                } else {
                    0
                }
            }
        };

        self.latest_offset += padding_bytes;

        let padding_layout = if self.is_packed || is_union {
            None
        } else {
            let force_padding = self.ctx.options().force_explicit_padding;

            // Otherwise the padding is useless.
            let need_padding = force_padding ||
                padding_bytes >= field_layout.align ||
                field_layout.align > MAX_GUARANTEED_ALIGN;

            debug!(
                "Offset: <padding>: {} -> {}",
                self.latest_offset - padding_bytes,
                self.latest_offset
            );

            debug!(
                "align field {field_name} to {}/{} with {padding_bytes} padding bytes {field_layout:?}",
                self.latest_offset,
                field_offset.unwrap_or(0) / 8,
            );

            let padding_align = if force_padding {
                1
            } else {
                cmp::min(field_layout.align, MAX_GUARANTEED_ALIGN)
            };

            if need_padding && padding_bytes != 0 {
                Some(Layout::new(padding_bytes, padding_align))
            } else {
                None
            }
        };

        if is_union {
            self.latest_offset =
                cmp::max(self.latest_offset, field_layout.size);
        } else {
            self.latest_offset += field_layout.size;
        }
        self.latest_field_layout = Some(field_layout);
        self.max_field_align =
            cmp::max(self.max_field_align, field_layout.align);
        self.last_field_was_bitfield = false;

        debug!(
            "Offset: {field_name}: {} -> {}",
            self.latest_offset - field_layout.size,
            self.latest_offset
        );

        padding_layout.map(|layout| self.padding_field(layout))
    }

    pub(crate) fn add_tail_padding(
        &mut self,
        comp_name: &str,
        comp_layout: Layout,
    ) -> Option<proc_macro2::TokenStream> {
        // Only emit an padding field at the end of a struct if the
        // user configures explicit padding.
        if !self.ctx.options().force_explicit_padding {
            return None;
        }

        // Padding doesn't make sense for rust unions.
        if self.is_rust_union {
            return None;
        }

        // Also doesn't make sense for structs with flexible array members
        if self.last_field_was_flexible_array {
            return None;
        }

        if self.latest_offset == comp_layout.size {
            // This struct does not contain tail padding.
            return None;
        }

        trace!(
            "need a tail padding field for {comp_name}: offset {} -> size {}",
            self.latest_offset,
            comp_layout.size
        );
        let size = comp_layout.size - self.latest_offset;
        Some(self.padding_field(Layout::new(size, 0)))
    }

    pub(crate) fn pad_struct(
        &mut self,
        layout: Layout,
    ) -> Option<proc_macro2::TokenStream> {
        debug!("pad_struct:\n\tself = {self:#?}\n\tlayout = {layout:#?}");

        if layout.size < self.latest_offset {
            warn!(
                "Calculated wrong layout for {}, too more {} bytes",
                self.name,
                self.latest_offset - layout.size
            );
            return None;
        }

        let padding_bytes = layout.size - self.latest_offset;
        if padding_bytes == 0 {
            return None;
        }

        let repr_align = true;

        // We always pad to get to the correct size if the struct is one of
        // those we can't align properly.
        //
        // Note that if the last field we saw was a bitfield, we may need to pad
        // regardless, because bitfields don't respect alignment as strictly as
        // other fields.
        if padding_bytes >= layout.align ||
            (self.last_field_was_bitfield &&
                padding_bytes >= self.latest_field_layout.unwrap().align) ||
            (!repr_align && layout.align > MAX_GUARANTEED_ALIGN)
        {
            let layout = if self.is_packed {
                Layout::new(padding_bytes, 1)
            } else if self.last_field_was_bitfield ||
                layout.align > MAX_GUARANTEED_ALIGN
            {
                // We've already given up on alignment here.
                Layout::for_size(self.ctx, padding_bytes)
            } else {
                Layout::new(padding_bytes, layout.align)
            };

            debug!("pad bytes to struct {}, {layout:?}", self.name);

            Some(self.padding_field(layout))
        } else {
            None
        }
    }

    pub(crate) fn requires_explicit_align(&self, layout: Layout) -> bool {
        let repr_align = true;

        // Always force explicit repr(align) for stuff more than 16-byte aligned
        // to work-around https://github.com/rust-lang/rust/issues/54341.
        //
        // Worst-case this just generates redundant alignment attributes.
        if repr_align && self.max_field_align >= 16 {
            return true;
        }

        if self.max_field_align >= layout.align {
            return false;
        }

        // We can only generate up-to a 8-bytes of alignment unless we support
        // repr(align).
        repr_align || layout.align <= MAX_GUARANTEED_ALIGN
    }

    fn padding_bytes(&self, layout: Layout) -> usize {
        align_to(self.latest_offset, layout.align) - self.latest_offset
    }

    fn padding_field(&mut self, layout: Layout) -> proc_macro2::TokenStream {
        let ty = helpers::blob(self.ctx, layout, false);
        let padding_count = self.padding_count;

        self.padding_count += 1;

        let padding_field_name = Ident::new(
            &format!("__bindgen_padding_{padding_count}"),
            Span::call_site(),
        );

        self.max_field_align = cmp::max(self.max_field_align, layout.align);

        let vis = super::access_specifier(self.visibility);

        quote! {
            #vis #padding_field_name : #ty ,
        }
    }

    /// Returns whether the new field is known to merge with a bitfield.
    ///
    /// This is just to avoid doing the same check also in `pad_field`.
    fn align_to_latest_field(&mut self, new_field_layout: Layout) -> bool {
        if self.is_packed {
            // Skip to align fields when packed.
            return false;
        }

        let Some(layout) = self.latest_field_layout else {
            return false;
        };

        // If it was, we may or may not need to align, depending on what the
        // current field alignment and the bitfield size and alignment are.
        debug!(
            "align_to_bitfield? {}: {layout:?} {new_field_layout:?}",
            self.last_field_was_bitfield,
        );

        // Avoid divide-by-zero errors if align is 0.
        let align = cmp::max(1, layout.align);

        if self.last_field_was_bitfield &&
            new_field_layout.align <= layout.size % align &&
            new_field_layout.size <= layout.size % align
        {
            // The new field will be coalesced into some of the remaining bits.
            //
            // FIXME(emilio): I think this may not catch everything?
            debug!("Will merge with bitfield");
            return true;
        }

        // Else, just align the obvious way.
        self.latest_offset += self.padding_bytes(layout);
        false
    }
}
